import { FileSystemAdapter, Menu, MenuItem, Notice, PaneType, Plugin, TAbstractFile, TFile, TFolder, Tasks, WorkspaceLeaf, addIcon, normalizePath, setIcon, setTooltip } from "obsidian";
import { JupyterEnvironment, JupyterEnvironmentError, JupyterEnvironmentEvent, JupyterEnvironmentStatus, JupyterEnvironmentType } from "./jupyter-env";
import { EmbeddedJupyterView } from "./ui/jupyter-view";
import { DEFAULT_SETTINGS, JupyterSettings, JupyterSettingsTab, OpenCreatedNotebook, PythonExecutableType } from "./jupyter-settings";
import { JupyterModal } from "./ui/jupyter-modal";
import { UpdateModal } from "./ui/jupyter-update-modal";
import { existsSync, rmdirSync } from "fs";
import { JupyterAbstractPath } from "./utils/jupyter-path";

export default class JupyterNotebookPlugin extends Plugin {

	/*=====================================================*/
	/* Plugin instance properties                          */
	/*=====================================================*/

	public settings: JupyterSettings = DEFAULT_SETTINGS;
	private serverRibbonIcon: HTMLElement|null = null;
	private fileRibbonIcon: HTMLElement|null = null;

	private onFileContextMenu = this.onFileContextMenuOpened.bind(this);

	public readonly env: JupyterEnvironment = new JupyterEnvironment(
		(this.app.vault.adapter as FileSystemAdapter).getBasePath(),
		DEFAULT_SETTINGS.debugConsole,
		DEFAULT_SETTINGS.pythonExecutable === PythonExecutableType.PYTHON ? "python" : DEFAULT_SETTINGS.pythonExecutablePath,
		DEFAULT_SETTINGS.jupyterTimeoutMs,
		DEFAULT_SETTINGS.jupyterEnvType,
		null,
		DEFAULT_SETTINGS.useSimpleMode
	);
	private envProperlyInitialized = false;
	private startEnvOnceInitialized = false;


	/*=====================================================*/
	/* Obsidian hooks (load, unload)                       */
	/*=====================================================*/

    async onload() {
		await this.loadSettings();
		this.env.printDebugMessages(this.settings.debugConsole);
		this.env.setPythonExecutable(this.settings.pythonExecutable === PythonExecutableType.PYTHON ? "python" : this.settings.pythonExecutablePath);
		this.env.setJupyterTimeoutMs(this.settings.jupyterTimeoutMs);
		this.env.setType(this.settings.jupyterEnvType);
		if (this.settings.deleteCheckpoints) {
			this.env.setCustomConfigFolderPath(this.getPluginFolder().getAbsolutePath());
		}
		this.env.on(JupyterEnvironmentEvent.CHANGE, this.showStatusMessage.bind(this));
		this.env.on(JupyterEnvironmentEvent.CHANGE, this.updateRibbon.bind(this));
		this.env.on(JupyterEnvironmentEvent.ERROR, this.onEnvironmentError.bind(this));
		this.env.on(JupyterEnvironmentEvent.EXIT, this.onJupyterExit.bind(this));
		this.envProperlyInitialized = true;
		if (this.startEnvOnceInitialized) {
			this.toggleJupyter();
		}

		/*
		 * The icon used is derived from `coreui`'s Jupyter SVG Vector icon :
		 * https://www.svgrepo.com/svg/341956/jupyter
		 * 
		 * It was adjusted to be a custom icon in Obsidian :
		 * - Removed the surrounding SVG tags
		 * - Resized to fit a viewBox of "0 0 100 100"
		 * - Added the fill="currentColor" property to adapt to Obsidian's theme
		 * For more information about those modifications, see Obsidian's documentation :
		 * https://docs.obsidian.md/Plugins/User+interface/Icons#Add+your+own+icon
		 * 
		 * Provided under the terms of the GPL license :
		 * https://www.svgrepo.com/page/licensing/#GPL
		 */
		addIcon("jupyter-logo", `<path fill="currentColor" d="m 51.4537,74.98344 c -15.406714,0 -29.180954,-5.68784 -36.479994,-13.79248 2.83328,7.29904 7.71248,13.79248 14.187674,18.24 6.493446,4.46576 14.187686,6.8856 22.29232,6.8856 8.10464,0 15.82016,-2.41984 22.29232,-6.8856 C 80.239467,74.98344 85.100427,68.49 87.933707,61.19096 80.634667,69.29864 66.86042,74.98344 51.4537,74.98344 Z m 0,-53.5192 c 15.40672,0 29.180967,5.68784 36.480007,13.79248 -2.83328,-7.29904 -7.69424,-13.79248 -14.187687,-18.24 -6.8856,-4.86096 -14.57984,-7.29904 -22.29232,-7.29904 -8.107674,0 -15.798874,2.44112 -22.29232,6.8856 C 22.689226,21.46424 17.806986,27.54424 14.973706,35.25672 22.272746,26.7356 35.654826,21.46424 51.4537,21.46424 Z M 79.829067,2.02344 c -7.566567,0 -7.566567,11.33312 0,11.33312 7.56656,0 7.56656,-11.33312 0,-11.33312 z M 22.689226,83.89672 c -4.04016,0 -7.299046,3.25888 -7.299046,7.29904 0,4.02192 3.258886,7.2808 7.299046,7.2808 4.021914,0 7.280794,-3.25888 7.280794,-7.2808 0,-4.04016 -3.258874,-7.29904 -7.280794,-7.29904 z m -6.08,-72.96 c -5.414243,0 -5.414243,8.10768 0,8.10768 5.399034,0 5.399034,-8.10768 0,-8.10768 z" id="path1" style="stroke-width:3.04" />`);

		if (this.settings.displayServerRibbonIcon) {
			this.serverRibbonIcon = this.addRibbonIcon("monitor-play", "Start Jupyter Server", this.toggleJupyter.bind(this));
		}
		if (this.settings.displayFileRibbonIcon) {
			this.fileRibbonIcon = this.addRibbonIcon("jupyter-logo", "Create Jupyter Notebook", this.onFileRibbonIconClicked.bind(this));
		}
		if (this.settings.displayFolderContextMenuItem) {
			this.app.workspace.on('file-menu', this.onFileContextMenu);
		}
		this.addCommand({
			id: "jupyter-create-notebook",
			name: "Create new Jupyter notebook",
			callback: (async () => {
				await this.createJupyterNotebook(JupyterAbstractPath.fromRelative("/", true, this.app.vault));
			}).bind(this)
		});

		this.registerView("jupyter-view", (leaf) => new EmbeddedJupyterView(leaf, this));
		this.registerExtensions(["ipynb"], "jupyter-view");
		this.addSettingTab(new JupyterSettingsTab(this.app, this));

		// Try to unload when Obsidian is closed by the user/the OS
		this.app.workspace.on('quit', async (_tasks: Tasks) => {
			await this.onunload();
		});

		this.announceUpdate();
	}

	async onunload() {
		await this.saveSettings();
		// Kill the Jupyter Notebook process
		this.env.exit();
		await this.purgeJupyterCheckpoints();
		this.app.workspace.off('file-menu', this.onFileContextMenu);
	}


	/*=====================================================*/
	/* UI Events (ribbon icon, server setting)             */
	/*=====================================================*/

	public async toggleJupyter() {
		// If the environment is not properly initialized, it cannot be started
		// This case can occur when the plugin is loading while views are also being loaded,
		// and one of them requests Jupyter to be started.
		if (!this.envProperlyInitialized) {
			this.startEnvOnceInitialized = true;
			return;
		}

		switch (this.env.getStatus()) {
			case JupyterEnvironmentStatus.EXITED:
				if (this.settings.deleteCheckpoints && !await this.customJupyterConfigExists()) {
					await this.generateJupyterConfig();
				}
				this.env.start();
				break;
			case JupyterEnvironmentStatus.RUNNING:
				this.env.exit();
				break;
		}
	}

	/**
	 * Restarts the Jupyter server if it is running.
	 * 
	 * If Jupyter is not running, it is simply started.
	 */
	public async restartJupyter() {
		if (this.env.getStatus() === JupyterEnvironmentStatus.EXITED) {
			this.toggleJupyter();
		}
		else {
			this.env.once(JupyterEnvironmentEvent.EXIT, (() => {
				this.toggleJupyter();
			}).bind(this));
			this.env.exit();
		}
	}


	/*=====================================================*/
	/* UI Events (ribbon icon, server setting)             */
	/*=====================================================*/

	/**
	 * Checks whether the plugin has been updated and displays a
	 * popup message if it has.
	 * 
	 * Strongly inspired from the QuickAdd implementation :
	 * https://github.com/chhoumann/quickadd/blob/08f269393c3cec5bf0c1d64a79d7999afd0a35a9/src/main.ts#L210
	 */
	private announceUpdate() {
		const currentVersion = this.manifest.version;
		const knownVersion = this.settings.knownVersion;

		// The version setting hasn't been set yet, the plugin has just been installed
		if (knownVersion === "") {
			return;
		}

		// The current version has already been announced
		if (knownVersion === currentVersion) {
			return;
		}

		this.settings.knownVersion = currentVersion;
		void this.saveSettings();

		if (!this.settings.updatePopup) return;

		const updateModal = new UpdateModal(this.app, this, knownVersion, currentVersion);
		updateModal.open();
	}


	/*=====================================================*/
	/* Settings (load, save, set values)                   */
	/*=====================================================*/

	private async loadSettings() {
		this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData());
		if (this.settings.checkpointsFolder !== "" && !this.settings.checkpointsFolder.endsWith('/')) {
			this.settings.checkpointsFolder += '/';
			await this.saveSettings();
		}
	}

	public async setPythonExecutable(value: PythonExecutableType) {
		this.settings.pythonExecutable = value;
		await this.saveSettings();
		switch (value) {
			case PythonExecutableType.PYTHON:
				this.env.setPythonExecutable("python");
				break;
			case PythonExecutableType.PATH:
				this.env.setPythonExecutable(this.settings.pythonExecutablePath);
				break;
		}
	}

	public async setPythonExecutablePath(value: string) {
		this.settings.pythonExecutablePath = value;
		await this.saveSettings();
		if (this.settings.pythonExecutable === PythonExecutableType.PATH) {
			this.env.setPythonExecutable(value);
		}
	}

	public async setStartJupyterAuto(value: boolean) {
		this.settings.startJupyterAuto = value;
		await this.saveSettings();
	}

	public async setJupyterEnvType(value: JupyterEnvironmentType) {
		this.settings.jupyterEnvType = value;
		await this.saveSettings();
		this.env.setType(value);
	}

	public async setUseSimpleMode(value: boolean) {
		this.settings.useSimpleMode = value;
		await this.saveSettings();
		this.env.setUseSimpleMode(value);
	}

	public async setDeleteCheckpoints(value: boolean) {
		this.settings.deleteCheckpoints = value;
		await this.saveSettings();
		if (value) {
			await this.generateJupyterConfig();
			this.env.setCustomConfigFolderPath(this.getPluginFolder().getAbsolutePath());
		}
		else {
			await this.deleteJupyterConfig();
			this.env.setCustomConfigFolderPath(null);
		}
	}
	
	public async setMoveCheckpointsToTrash(value: boolean) {
		this.settings.moveCheckpointsToTrash = value;
		await this.saveSettings();
	}

	public async setCheckpointsFolder(value: string) {
		// Make sure the provided checkpoints folder ends with '/'
		if (value !== "" && !value.endsWith('/')) {
			value += '/';
		}
		this.settings.checkpointsFolder = value;
		await this.saveSettings();
		// Update the custom Jupyter config to take into account the new checkpoints folder
		// (requires Jupyter to be restarted)
		if (this.settings.deleteCheckpoints) {
			await this.generateJupyterConfig();
		}
	}

	public async setUpdatePopup(value: boolean) {
		this.settings.updatePopup = value;
		await this.saveSettings();
	}

	public async setServerRibbonIconSetting(value: boolean) {
		this.settings.displayServerRibbonIcon = value;
		await this.saveSettings();
		if (!value) {
			this.serverRibbonIcon?.remove();
			this.serverRibbonIcon = null;
		}
		else {
			this.serverRibbonIcon = this.addRibbonIcon("monitor-play", "Start Jupyter Server", this.toggleJupyter.bind(this));
			this.updateRibbon(this.env);
		}
	}

	public async setStatusNoticesSetting(value: boolean) {
		this.settings.useStatusNotices = value;
		await this.saveSettings();
	}

	public async setFileRibbonIconSetting(value: boolean) {
		this.settings.displayFileRibbonIcon = value;
		await this.saveSettings();
		if (!value) {
			this.fileRibbonIcon?.remove();
			this.fileRibbonIcon = null;
		}
		else {
			this.fileRibbonIcon = this.addRibbonIcon("jupyter-logo", "Create Jupyter Notebook", this.onFileRibbonIconClicked.bind(this));
		}
	}

	public async setFolderContextMenuSetting(value: boolean) {
		this.settings.displayFolderContextMenuItem = value;
		await this.saveSettings();
		if (!value) {
			this.app.workspace.off('file-menu', this.onFileContextMenu);
		}
		else {
			this.app.workspace.on('file-menu', this.onFileContextMenu);
		}
	}

	public async setOpenCreatedFileMode(value: OpenCreatedNotebook) {
		this.settings.openCreatedFileMode = value;
		await this.saveSettings();
	}

	public async setJupyterTimeoutMs(value: number) {
		this.settings.jupyterTimeoutMs = value;
		await this.saveSettings();
		this.env.setJupyterTimeoutMs(value);
	}

	public async setDebugConsole(value: boolean) {
		this.settings.debugConsole = value;
		await this.saveSettings();
		this.env.printDebugMessages(this.settings.debugConsole);
	}

	public async saveSettings() {
		await this.saveData(this.settings);
	}


	/*=====================================================*/
	/* Jupyter Environment event (on change, error, exit)  */
	/*=====================================================*/

	private showStatusMessage() {
		if (!this.settings.useStatusNotices) {
			return;
		}
		
		switch (this.env.getStatus()) {
			case JupyterEnvironmentStatus.STARTING:
				new Notice("Jupyter Server is starting");
				break;
			case JupyterEnvironmentStatus.RUNNING:
				new Notice("Jupyter Server is now running");
				break;
			case JupyterEnvironmentStatus.EXITED:
				new Notice("Jupyter Server has exited");
				break;
		}
	
	}

	private onEnvironmentError(_env: JupyterEnvironment, error: JupyterEnvironmentError) {
		if (error === JupyterEnvironmentError.JUPYTER_STARTING_TIMEOUT) {
			new JupyterModal(
				this.app,
				"Jupyter Timeout",
				[
					"The Jupyter server took too long to start.",
					"You can set in the settings the maximum time the plugin will wait for the server to start.",
					"Your current timeout is set to " + (this.settings.jupyterTimeoutMs / 1000) + " second(s).",
					this.settings.jupyterTimeoutMs < 15000 ? "This is a very short timeout and might not be enough for the server to start. Please try increasing it and see if the error disappears." : "This timeout seems reasonable, hence the problem might be elsewhere depending on your specific situation."
				],
				[
					{
						text: "Open troubleshooting guide",
						onClick: () => { window.open("https://jupyter.mael.im/troubleshooting#jupyter-timeout", "_blank"); },
						closeOnClick: false
					}
				]
			).open();
		}
		else if (error === JupyterEnvironmentError.UNABLE_TO_START_JUPYTER) {
			new JupyterModal(
				this.app,
				"Couldn't start Jupyter",
				[
					"Jupyter could not even be started.",
					"Please check your Python executable and make sure Jupyter is installed in the corresponding environment.",
					"Use the button below to open the troubleshooting guide."
				],
				[
					{
						text: "Open troubleshooting guide",
						onClick: () => { window.open("https://jupyter.mael.im/troubleshooting#jupyter-process-could-not-be-spawned", "_blank"); },
						closeOnClick: false
					}
				]
			)
		}
		else if (error === JupyterEnvironmentError.JUPYTER_EXITED_WITH_ERROR) {
			new JupyterModal(
				this.app,
				"Jupyter crashed",
				[
					"Jupyter crashed while starting",
					"Use the button below to open the troubleshooting guide.",
					"Here is the last log message from Jupyter:",
					this.env.getLastLog()
				],
				[
					{
						text: "Open troubleshooting guide",
						onClick: () => { window.open("https://jupyter.mael.im/troubleshooting#jupyter-process-crashed", "_blank"); },
						closeOnClick: false
					}
				]
			).open();
		}
		else {
			new JupyterModal(
				this.app,
				"Jupyter exited",
				[
					"Jupyter crashed while starting but did not encounter an error.",
					"This is a very rare case and might be due to an 'exit()' statement that got lost in your Jupyter configuration.",
					"Use the button below to open the troubleshooting guide.",
					"Here is the last log message from Jupyter:",
					this.env.getLastLog()
				],
				[
					{
						text: "Open troubleshooting guide",
						onClick: () => { window.open("https://jupyter.mael.im/troubleshooting#jupyter-process-exited", "_blank"); },
						closeOnClick: false
					}
				]
			).open();
		
		}
	}

	private async updateRibbon(env: JupyterEnvironment) {
		if (this.serverRibbonIcon === null || !this.settings.displayServerRibbonIcon) {
			return;
		}

		switch (env.getStatus()) {
			case JupyterEnvironmentStatus.STARTING:
				setIcon(this.serverRibbonIcon as HTMLElement, "monitor-dot");
				setTooltip(this.serverRibbonIcon as HTMLElement, "Jupyter Server is starting");
				break;
			case JupyterEnvironmentStatus.RUNNING:
				setIcon(this.serverRibbonIcon as HTMLElement, "monitor-stop");
				setTooltip(this.serverRibbonIcon as HTMLElement, "Stop Jupyter Server");
				break;
			case JupyterEnvironmentStatus.EXITED:
				setIcon(this.serverRibbonIcon as HTMLElement, "monitor-play");
				setTooltip(this.serverRibbonIcon as HTMLElement, "Start Jupyter Server");
				break;
		}
	}

	private async onJupyterExit(_env: JupyterEnvironment) {
		await this.purgeJupyterCheckpoints();
	}


	/*=====================================================*/
	/* Jupyter checkpoints management                      */
	/*=====================================================*/

	public async onFileRibbonIconClicked() {
		await this.createJupyterNotebook(JupyterAbstractPath.fromRelative("/", true, this.app.vault));
	}

	public onFileContextMenuOpened(menu: Menu, file: TAbstractFile, _source: string, _leaf?: WorkspaceLeaf) {
		// Only propose to create a Jupyter Notebook in folders
		if (file instanceof TFolder) {
			menu.addItem((item: MenuItem) => {
				item
					.setTitle("New Jupyter notebook")
					.setIcon("jupyter-logo")
					.setSection("action-primary")
					.onClick(async (_event: MouseEvent|KeyboardEvent) => {
						await this.createJupyterNotebook(JupyterAbstractPath.fromRelative(file.path, true, this.app.vault));
					});
			});
		}
	}

	private getDefaultNotebookFilename(): string {
		const now = new Date();
		const year = now.getFullYear();
		const month = String(now.getMonth() + 1).padStart(2, '0');
		const day = String(now.getDate()).padStart(2, '0');
		const hours = String(now.getHours()).padStart(2, '0');
		const minutes = String(now.getMinutes()).padStart(2, '0');
		const seconds = String(now.getSeconds()).padStart(2, '0');
		return `Jupyter Notebook ${year}-${month}-${day}-${hours}-${minutes}-${seconds}.ipynb`;
	}

	private async createJupyterNotebook(folder: JupyterAbstractPath) {
		// Check that the notebook is being created inside of the Obsidian vault
		if (!folder.inVault()) {
			throw new Error("Creating a new notebook can only be done within the vault.");
		}

		// Append the filename to the folder name
		const file = folder.append(this.getDefaultNotebookFilename(), false);

		// Check that the file does not already exist to avoid overwriting it
		if (await this.app.vault.adapter.exists(file.getRelativePath() as string)) {
			new Notice(`The file "${file.getRelativePath() as string}" already exists, creation was aborted to avoid overwriting it. Please try again.`);
		}

		// Create a Jupyter notebook with the minimum amount of content
		await this.app.vault.adapter.write(
			file.getRelativePath() as string,
			`{"cells": [],"metadata": {"kernelspec": {"display_name": "","name": ""},"language_info": {"name": ""}},"nbformat": 4,"nbformat_minor": 5}`
		);

		// Depending on the corresponding setting, open the created notebook
		if (this.settings.openCreatedFileMode !== OpenCreatedNotebook.DONT) {
			let newLeaf: PaneType|boolean;
			switch (this.settings.openCreatedFileMode) {
				case OpenCreatedNotebook.CURRENT_TAB:
					newLeaf = false;
					break;
				case OpenCreatedNotebook.NEW_TAB:
					newLeaf = 'tab';
					break;
				case OpenCreatedNotebook.SPLIT:
					newLeaf = 'split';
					break;
				case OpenCreatedNotebook.WINDOW:
					newLeaf = 'window';
					break;
			}
			const leaf = this.app.workspace.getLeaf(newLeaf);
			leaf.openFile(this.app.vault.getFileByPath(file.getRelativePath() as string) as TFile);
		}
	}


	/*=====================================================*/
	/* Jupyter checkpoints management                      */
	/*=====================================================*/

	private async purgeJupyterCheckpoints() {
		// Find what the folder to delete is, where the checkpoints are stored
		let checkpointsFolder: JupyterAbstractPath;
		try {
			checkpointsFolder = this.getCheckpointsRootFolder();
		}
		catch (e: any) {
			// The root folder of the Jupyter checkpoints cannot be found, most probably
			// because the plugin is being executed on mobile.
			return;
		}

		// Check that the folder exists
		if (!existsSync(checkpointsFolder.getAbsolutePath())) {
			return;
		}

		// If the root checkpoints folder was found, delete it
		if (!this.settings.deleteCheckpoints || this.settings.moveCheckpointsToTrash) {
			// Even if the setting is disabled, we do not want to keep the
			// special checkpoints folder around, but we move it to the bin so
			// that it is still recoverable.

			// Trashing is only possible inside of the vault for now
			if (checkpointsFolder.inVault()) {
				this.app.vault.adapter.trashSystem(checkpointsFolder.getRelativePath() as string);
			}
			else {
				new Notice("[Jupyter for Obsidian] ERROR\n\nMoving the Jupyter checkpoints to the " +
					"system trash is only possible when the checkpoints are stored inside of the vault.\n\n" +
					"Please consider changing either the checkpoints folder path setting to one that is inside " +
					"the vault, or define the checkpoints to be deleted without going to the trash.\n\n" +
					"Your checkpoints were not deleted nor moved to the trash.", 0);
			}
		}
		else {
			// Prefer to use the Obsidian's vault adapter where possible
			if (checkpointsFolder.inVault()) {
				this.app.vault.adapter.rmdir(checkpointsFolder.getRelativePath() as string, true);
			}
			else {
				rmdirSync(checkpointsFolder.getAbsolutePath(), { recursive: true });
			}
		}
	}

	/**
	 * Obsidian plugins are installed in the Obsidian's settings folder, in their
	 * own folder named after themselves.
	 * 
	 * This method provides the path to the folder that hosts Jupyter for Obsidian
	 * and its code, settings and configuration.
	 */
	public getPluginFolder(): JupyterAbstractPath {
		return JupyterAbstractPath.fromRelative(
			this.app.vault.configDir + "/plugins/" + this.manifest.id + "/",
			true,
			this.app.vault
		);
	}

	/**
	 * Provides the default Jupyter checkpoints folder. If the user did not provide any
	 * custom value, the Jupyter checkpoints will be stored in the plugin's settings directory
	 * before being deleted.
	 */
	public getDefaultCheckpointsRootFolder(): JupyterAbstractPath {
		const pluginFolder: JupyterAbstractPath = this.getPluginFolder();
		return pluginFolder.append(".ipynb_checkpoints/", true);
	}

	/**
	 * Provide the path to the custom Jupyter config file ('jupyter_lab_config.py'). This
	 * configuration is used to tell Jupyter where to put checkpoints if the user has enabled
	 * auto-deletion of checkpoints.
	 */
	public getJupyterConfigPath(): JupyterAbstractPath {
		const pluginFolder: JupyterAbstractPath = this.getPluginFolder();
		return pluginFolder.append("jupyter_lab_config.py", false);
	}

	/**
	 * For the feature that gets rid of the Jupyter checkpoints, the plugin uses the Jupyter
	 * configuration to put all of the checkpoints in a separate folder. This function computes
	 * and returns the absolute (system) path to that folder, to pass it to Jupyter.
	 * 
	 * It takes both the default value and the possible user setting value into account.
	 * 
	 * @throws If the file adapter cannot be used to retrieve absolute paths (most probably because on mobile).
	 */
	private getCheckpointsRootFolder(): JupyterAbstractPath {
		// Check that absolute paths can be retrieved
		if (!(this.app.vault.adapter instanceof FileSystemAdapter)) {
			throw new Error("Invalid environment : need a file system adapter to work with files outside of the vault (Jupyter for Obsidian).");
		}
		
		// If the user setting has a value, use it
		if (this.settings.checkpointsFolder !== "") {
			return JupyterAbstractPath.fromAbsolute(
				this.settings.checkpointsFolder + ".ipynb_checkpoints",
				true,
				this.app.vault
			);
		}
		// Otherwise, use the default value
		else {
			// The default path is inside of the plugin's folder
			return this.getDefaultCheckpointsRootFolder();
		}
	}

	/**
	 * Indicates whether the Jupyter configuration file exists (true) or
	 * needs to be created (false).
	 */
	private async customJupyterConfigExists(): Promise<boolean> {
		// Find out the path to the Jupyter configuration file
		let configPath: JupyterAbstractPath;
		try { configPath = this.getJupyterConfigPath(); }
		catch (e) { return false; }

		// Check for the config to be in the vault
		if (!configPath.inVault()) {
			return false;
		}

		// Check if the file exists
		// We know configPath.getRelativePath() is not null because we checked for the config to be in the vault
		return await this.app.vault.adapter.exists(normalizePath(configPath.getRelativePath() as string));
	}

	/**
	 * Generates a Jupyter configuration file in the folder indicated by
	 * `getCustomJupyterConfigPath()` with settings to put all checkpoints
	 * in a single folder.
	 */
	private async generateJupyterConfig(): Promise<boolean> {
		// Find out the path to the Jupyter configuration file
		// Find where the checkpoints will have to be stored to then tell Jupyter
		let checkpointsFolder: JupyterAbstractPath;
		let configPath: JupyterAbstractPath;
		try {
			checkpointsFolder = this.getCheckpointsRootFolder();
			configPath = this.getJupyterConfigPath();
		}
		catch (e) { return false; }

		// The configuration file must be within the vault in order to use the vault's adapter
		if (!configPath.inVault()) {
			return false;
		}

		// Prepare the content to put into the configuration file
		const configContent = `c.FileContentsManager.checkpoints_kwargs = {'root_dir': r'${checkpointsFolder.getAbsolutePath()}'}
print("[Jupyter for Obsidian] Custom configuration of Jupyter for Obsidian loaded successfully.")`

		// Write the config to the file
		// We know configPath.getRelativePath() is not null because we checked it is in the vault above
		await this.app.vault.adapter.write(normalizePath(configPath.getRelativePath() as string), configContent);
		return true;
	}

	/**
	 * If the custom Jupyter configuration file used by the Jupyter for Obsidian
	 * plugin exists, it deletes it.
	 */
	private async deleteJupyterConfig() {
		// Find out the path to the Jupyter configuration file
		let configPath: JupyterAbstractPath;
		try { configPath = this.getJupyterConfigPath(); }
		catch (e) { return; }

		// To use the vault adapter, the config path must be within the vault
		if (!configPath.inVault()) {
			return;
		}
		
		// Since we checked the config path is in the vault, it must have a relative path
		await this.app.vault.adapter.remove(normalizePath(configPath.getRelativePath() as string));
	}
}